/**
 * Copyright (C) 2013 Daniel Gee.
 * See LICENSE.txt for more details.
 */
package com.rl.dungeon;

import java.awt.Point;
import java.io.Serializable;

/**
 * Specifies a particular coordinate within 3d space. Use {@link Location#of(int, int, int)} or the
 * related methods to obtain Location instances.
 * 
 * <p>
 * If desired, a cache of Location instances can be created using {@link #setCacheEdge(Location)}
 * for all coordinates from 0,0,0 out to whatever non-negative coordinates are desired along each
 * axis (without including the edge itself). However, the cache is not used by default. Attempting
 * to access a Location outside of the cache simply causes a new Location to be created.
 * </p>
 * 
 * @author Daniel Gee
 */
public class Location implements Serializable
{
	/**
	 * The array of cached Location instances.
	 */
	private static Location[][][] cache = new Location[0][0][0];

	/**
	 * The "far" edge of the cached Location values. The near edge is always assumed to be 0,0,0.
	 */
	private static Location cacheEdge = new Location(0, 0, 0);

	/**
	 * 
	 */
	private static final long serialVersionUID = -4982670764883686764L;

	/**
	 * @return the current cache edge.
	 */
	public static Location getCacheEdge()
	{
		return Location.cacheEdge;
	}

	/**
	 * Returns a Location instance representing the x,y coordinates given.
	 * 
	 * @param x
	 *            the x component.
	 * @param y
	 *            the y component.
	 * @return the Location.
	 */
	public static Location of(int x, int y)
	{
		return Location.of(x, y, 0);
	}

	/**
	 * Returns a Location instance representing the x,y,z coordinates given.
	 * 
	 * @param x
	 *            the x component.
	 * @param y
	 *            the y component.
	 * @param z
	 *            the z component.
	 * @return the Location.
	 */
	public static Location of(int x, int y, int z)
	{
		if (x >= 0 && y >= 0 && z >= 0 && x < Location.cacheEdge.x && y < Location.cacheEdge.y
				&& z < Location.cacheEdge.z)
		{
			return Location.cache[x][y][z];
		}
		return new Location(x, y, z);
	}

	/**
	 * Returns a Location instance representing the x,y coordiantes of the {@link Point} given, with
	 * a z component of 0.
	 * 
	 * @param p
	 *            the Point in question
	 * @return a Location instance.
	 */
	public static Location of(Point p)
	{
		return Location.of(p.x, p.y, 0);
	}

	/**
	 * Assigns the far edge of the cache and regenerates the cache of Locations. This creates
	 * edge.x*edge.y*edge.z new Location instances all at once, so you probably just want to use
	 * this once at the start of your program. Note that if <b>any</b> of the coordinates of the
	 * cache edge are 0, then the <b>entire</b> cache will end up being empty. This method is
	 * synchronized.
	 * 
	 * <p>
	 * Generally, if you wanted to use it, you'd simply set the cache to be the size of the standard
	 * dungeon that your game creates at some point during your program's startup. For example,
	 * using the line<br />
	 * <tt>Location.setCacheEdge(Location.of(DUNGEON_HEIGHT,DUNGEON_WIDTH,AMULET_DEPTH));</tt><br />
	 * near the start of your program's main method.
	 * </p>
	 * 
	 * @param edge
	 *            a Location representing how many indices worth of Location should be cached along
	 *            the x, y, and z axis (0-indexed, like an array).
	 * @throws IllegalArgumentException
	 *             if any of the edge's components are negative.
	 */
	public static synchronized void setCacheEdge(Location edge)
	{
		if (edge.x < 0 || edge.y < 0 || edge.z < 0)
		{
			throw new IllegalArgumentException(
					"All coordinate components must be non-negative. Illegal Edge:" + edge);
		}
		Location.cacheEdge = edge;
		Location.cache = new Location[edge.x][edge.y][edge.z];
		for (int x = 0; x < edge.x; x++)
		{
			for (int y = 0; y < edge.y; y++)
			{
				for (int z = 0; z < edge.z; z++)
				{
					Location.cache[x][y][z] = new Location(x, y, z);
				}
			}
		}
	}

	/**
	 * The x component of this location.
	 */
	public final int x;

	/**
	 * The y component of this location.
	 */
	public final int y;

	/**
	 * The z component of this location.
	 */
	public final int z;

	/**
	 * Constructs a new location for the coordinates specified.
	 * 
	 * @param x
	 *            the x coordinate.
	 * @param y
	 *            the y coordinate.
	 * @param z
	 *            the z coordinate.
	 */
	private Location(int x, int y, int z)
	{
		this.x = x;
		this.y = y;
		this.z = z;
	}

	/**
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj)
	{
		if (this == obj)
		{
			return true;
		}
		if (obj == null)
		{
			return false;
		}
		if (this.getClass() != obj.getClass())
		{
			return false;
		}
		Location other = (Location) obj;
		if (this.x != other.x)
		{
			return false;
		}
		if (this.y != other.y)
		{
			return false;
		}
		if (this.z != other.z)
		{
			return false;
		}
		return true;
	}

	/**
	 * Returns the Euclidean Distance between this location and another location. It's the "real"
	 * distance between here and there.
	 * 
	 * @param other
	 *            the other location.
	 * @return the Euclidean distance.
	 */
	public double euclideanDistance(Location other)
	{
		return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2)
				+ Math.pow(this.z - other.z, 2));
	}

	/**
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode()
	{
		final int prime = 31;
		int result = 1;
		result = prime * result + this.x;
		result = prime * result + this.y;
		result = prime * result + this.z;
		return result;
	}

	/**
	 * Returns the Manhattan Distance between here and the location provided. It's the sum of the
	 * difference between here and there along each of the three axises.
	 * 
	 * @param other
	 *            the other location.
	 * @return the manhattan distance.
	 */
	public int manhattanDistance(Location other)
	{
		return Math.abs(this.x - other.x) + Math.abs(this.y - other.y) + Math.abs(this.z - other.z);
	}

	/**
	 * Returns the "roguelike" distance between here and the location provided. It's the maximum of
	 * the difference between the X, Y, and Z components. In other words: it's the number of steps a
	 * roguelike character would have to step to get from here to there. Since diagonals are 1:1 in
	 * roguelikes, we only take the single highest difference, we don't add them up like with
	 * {@link #manhattanDistance(Location)}.
	 * 
	 * @param other
	 *            the other location.
	 * @return the roguelike distance.
	 */
	public int rogueDistance(Location other)
	{
		return Math.max(Math.max(Math.abs(this.x - other.x), Math.abs(this.y - other.y)),
				Math.abs(this.z - other.z));
	}

	/**
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString()
	{
		return "Location(" + this.x + "," + this.y + "," + this.z + ")";
	}

	/**
	 * Returns a location that's the translation of this location a single unit in the specified
	 * direction.
	 * 
	 * @param dir
	 *            the direction to move in.
	 * @return the location.
	 */
	public Location translate(Direction dir)
	{
		return this.translate(dir.dx, dir.dy, dir.dz);
	}

	/**
	 * Returns a location that's the translation of this location by the amounts specified.
	 * 
	 * @param dx
	 *            the change in x
	 * @param dy
	 *            the change in y
	 * @return the location.
	 */
	public Location translate(int dx, int dy)
	{
		return this.translate(dx, dy, 0);
	}

	/**
	 * Returns a location that's the translation of this location by the amounts specified.
	 * 
	 * @param dx
	 *            the change in x
	 * @param dy
	 *            the change in y
	 * @param dz
	 *            the change in z
	 * @return the location.
	 */
	public Location translate(int dx, int dy, int dz)
	{
		return Location.of(this.x + dx, this.y + dy, this.z + dz);
	}
}
